package com.hb.web;

import com.hb.SpittleNotFoundException;
import com.hb.domain.Spittle;
import com.hb.repository.SpittleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import com.hb.Error;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.List;

@Controller
@RequestMapping("/spittles")
public class SpittleController {
    private static final String MAX_LONG_AS_STRING = "9223372036854775807";

    private SpittleRepository spittleRepository;

    @Autowired
    public SpittleController(SpittleRepository spittleRepository) {
        this.spittleRepository = spittleRepository;
    }

    /**
     * 当发起对 “/spittles” 的 GET 请求时，将会调用 spittles() 方法。它会查找并返回 Spittle 列表，而这个列表会通过注入的 SpittleRepository 获取到。
     * 列表会放到模型中，用于视图的渲染。对于基于浏览器的 Web 应用，这可能意味着模型数据会渲染到 HTML 页面中。但是，我们现在讨论的是创建 REST API。在这种情况下，
     * HTML 并不是合适的数据表述形式。
     *
     * @param max
     * @param count
     * @return
     */
    @RequestMapping(method = RequestMethod.GET)
    // 告诉 Spring 跳过正常的模型/视图流程，并使用消息转换器最简单的方法是为控制器方法添加 @ResponseBody 注解
    public @ResponseBody
    List<Spittle> spittles(
            @RequestParam(value = "max", defaultValue = MAX_LONG_AS_STRING) long max,
            @RequestParam(value = "count", defaultValue = "20") int count) {
        return spittleRepository.findSpittles(max, count);
    }

    /**
     * 假设我们需要一种方式将客户端提交的新 Spittle 保存起来。我们可以按照如下的方式编写控制器方法来处理这种请求。
     *
     * @param spittle
     * @return
     * @RequestMapping 表明它只能处理 “/spittles”（在类级别的 @RequestMapping 中进行了声明）的 POST 请求。POST 请求体中预期要包含一个 Spittle 的资源表述。
     * 因为 Spittle 参数上使用了 @RequestBody，所以 Spring 将会查看请求中的 Content-Type 头部信息，并查找能够将请求体转换为 Spittle 的消息转换器。
     * 例如，如果客户端发送的 Spittle 数据是 JSON 表述形式，那么 Content-Type 头部信息可能就会是 “application/json”。在这种情况下，DispatcherServlet 会
     * 查找能够将 JSON 转换为 Java 对象的消息转换器。如果 Jackson 2 库在类路径中，那么 MappingJackson2HttpMessageConverter 将会担此重任，将 JSON 表述转换
     * 为 Spittle，然后传递到 saveSpittle() 方法中。这个方法还使用了 @ResponseBody 注解，因此方法返回的 Spittle 对象将会转换为某种资源表述，发送给客户端。
     */
    // 注意，@RequestMapping 有一个 consumes 属性，我们将其设置为 “application/json”。consumes 属性的工作方式类似于 produces，不过它会关注请求的
    // Content-Type 头部信息。它会告诉 Spring 这个方法只会处理对 “/spittles” 的 POST 请求，并且要求请求的 Content-Type 头部信息为 “application/json”。
    // 如果无法满足这些条件的话，会由其他方法（如果存在合适的方法的话）来处理请求。
    @RequestMapping(method = RequestMethod.POST, consumes = "application/json")
    public @ResponseBody
    Spittle saveSpittle(@RequestBody Spittle spittle) {
        return spittleRepository.save(spittle);
    }

    /**
     * 在 saveSpittle() 处理完请求之后，服务器在响应体中包含了 Spittle 的表述以及 HTTP 状态码 200（OK），将其返回给客户端。
     * 这里没有什么大问题，但是还不是完全准确。当然，假设处理请求的过程中成功创建了资源，状态可以视为 OK。但是，我们不仅仅需要说“OK”。
     * 我们创建了新的内容，HTTP 状态码也将这种情况告诉给了客户端。不过，HTTP 201 不仅能够表明请求成功完成，而且还能描述创建了新资源。
     * 如果我们希望完整准确地与客户端交流，那么响应是不是应该为 201（Created），而不仅仅是 200（OK）呢？但这只是问题的一部分。客户端
     * 知道新创建了资源，你觉得客户端会不会感兴趣新创建的资源在哪里呢？毕竟，这是一个新创建的资源，会有一个新的 URL 与之关联。难道客户端
     * 只能猜测新创建资源的 URL 是什么吗？我们能不能以某种方式将其告诉客户端？当创建新资源的时候，将资源的 URL 放在响应的 Location
     * 头部信息中，并返回给客户端是一种很好的方式。因此，我们需要有一种方式来填充响应头部信息，此时我们的老朋友 ResponseEntity 就能提
     * 供帮助了。这是在其他的注解或异常处理器做不到的。
     * @param spittle
     * @return
     */
    @RequestMapping(method = RequestMethod.POST, consumes = "application/json")
    // 为了使用 UriComponentsBuilder，我们需要做的就是在处理器方法中将其作为一个参数。在处理器方法所得到的 UriComponentsBuilder 中，会预先配置已
    // 知的信息如 host、端口以及 Servlet 内容。它会从处理器方法所对应的请求中获取这些基础信息。基于这些信息，代码会通过设置路径的方式构建 UriComponents 其余的部分。
    public ResponseEntity<Spittle> saveSpittle1(@RequestBody Spittle spittle, UriComponentsBuilder ucb) {
        Spittle savedSpittle = spittleRepository.save(spittle);
        // HttpHeaders 是 MultiValueMap<String, String> 的特殊实现，它有一些便利的 Setter 方法（如 setLocation()），
        // 用来设置常见的 HTTP 头部信息。
        HttpHeaders headers = new HttpHeaders();
        // 使用硬编码值的方式来构建 Location 头部信息。URL 中 “localhost” 以及 “8080” 这两个部分尤其需要注意，因为如果我
        // 们将应用部署到其他地方，而不是在本地运行的话，它们就不适用了。我们其实没有必要手动构建 URL，Spring 提供了 UriComponentsBuilder，
        // 可以给我们一些帮助。它是一个构建类，通过逐步指定 URL 中的各种组成部分（如 host、端口、路径以及查询），我们能够使用它来构建 UriComponents 实例。
        // 借助 UriComponentsBuilder 所构建的 UriComponents 对象，我们就能获得适合设置给 Location 头部信息的 URI。
//        URI locationUri = URI.create("http://localhost:8080/spittr/spittles/"+spittle.getId());

        // 注意，路径的构建分为两步。第一步调用 path() 方法，将其设置为 “/ spittles/”，也就是这个控制器所能处理的基础路径。然后，
        // 在第二次调用 path() 的时候，使用了已保存 Spittle 的 ID。我们可以推断出来，每次调用 path() 都会基于上次调用的结果。在路径设置完成之后，
        // 调用 build() 方法来构建 UriComponents 对象，根据这个对象调用 toUri() 就能得到新创建 Spittle 的 URI。
        URI locationUri = ucb.path("/spittles").path(String.valueOf(spittle.getId())).build().toUri();
                headers.setLocation(locationUri);
        // 当返回 ResponseEntity 时，在响应中设置头部信息
        ResponseEntity<Spittle> responseEntity = new ResponseEntity<Spittle>(savedSpittle, headers, HttpStatus.CREATED);
        return responseEntity;
    }

    /**
     * 如果根据给定的 ID，无法找到某个 Spittle 对象的 ID 属性能够与之匹配，findOne() 方法返回 null 的时候，你觉得会发生什么呢？结果就是 spittleById() 方法会返回 null，
     * 响应体为空，不会返回任何有用的数据给客户端。同时，响应中默认的 HTTP 状态码是 200（OK），表示所有的事情运行正常。但是，所有的事情都是不对的。客户端要求 Spittle 对象，但是它
     * 什么都没有得到。现在，我们考虑一下在这种场景下应该发生什么。至少，状态码不应该是 200，而应该是 404（Not Found），告诉客户端它们所要求的内容没有找到。如果响应体中能够包含
     * 错误信息而不是空的话就更好了。Spring 提供了多种方式来处理这样的场景：
     * 1、使用 @ResponseStatus 注解可以指定状态码；
     * 2、控制器方法可以返回 ResponseEntity 对象，该对象能够包含更多响应相关的元数据；
     * 3、异常处理器能够应对错误场景，这样处理器方法就能关注于正常的状况。
     * 在这个方面，Spring 提供了很多的灵活性，其实也不存在唯一正确的方式。
     *
     * @param id
     * @return
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public @ResponseBody
    Spittle spittleById(@PathVariable long id) {
        return spittleRepository.findOne(id);
    }

    /**
     * 作为 @ResponseBody 的替代方案，控制器方法可以返回一个 ResponseEntity 对象。ResponseEntity 中可以包含响应相关的元数据（如头部信息和状态码）以及要转换成资源表述的对象。
     * 因为 ResponseEntity 允许我们指定响应的状态码，所以当无法找到 Spittle 的时候，我们可以返回 HTTP 404 错误。注意这个 spittleById() 方法没有使用 @ResponseBody 注解。
     * 除了包含响应头信息、状态码以及负载以外，ResponseEntity 还包含了 @ResponseBody 的语义，因此负载部分将会渲染到响应体中，就像之前在方法上使用 @ResponseBody 注解一样。
     * 如果返回 ResponseEntity 的话，那就没有必要在方法上使用 @ResponseBody 注解了。
     *
     * @param id
     * @return
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public ResponseEntity<Spittle> spittleById1(@PathVariable long id) {
        Spittle spittle = spittleRepository.findOne(id);
        HttpStatus status = spittle != null ? HttpStatus.OK : HttpStatus.NOT_FOUND;
        return new ResponseEntity<Spittle>(spittle, status);
    }

    /**
     * 如下可以在响应体中包含一些错误信息。但是，还有些问题。首先，这比我们开始的时候更为复杂。涉及到了更多的逻辑，包括条件语句。另外，方法返回 ResponseEntity<?> 感觉有些问
     * 题。ResponseEntity 所使用的泛型为它的解析或出现错误留下了太多的空间。不过，我们可以借助错误处理器来修正这些问题。spittleById() 方法中的 if 代码块是处理错误的，
     * 但这是控制器中错误处理器（error handler）所擅长的领域。错误处理器能够处理导致问题的场景，这样常规的处理器方法就能只关心正常的逻辑处理路径了。
     * @param id
     * @return
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public ResponseEntity<?> spittleById2(@PathVariable long id) {
        Spittle spittle = spittleRepository.findOne(id);
        if (spittle == null) {
            Error error = new Error(4, "Spittle [" + id + "] not found");
            return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
        }
        return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
    }

    /**
     * 定义能够对应 SpittleNotFoundException 的错误处理器。
     * @ExceptionHandler 注解能够用到控制器方法中，用来处理特定的异常。这里，它表明如果在控制器的任意处理方法中抛出 SpittleNotFoundException 异常，就会调
     * 用 spittleNotFound() 方法来处理异常。
     * @param e
     * @return
     */
    @ExceptionHandler(SpittleNotFoundException.class)
    public ResponseEntity<Error> spittleNotFound(SpittleNotFoundException e) {
        long spittleId = e.getSpittleId();
        Error error = new Error(4, "Spittle [" + spittleId + "] not found");
        return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
    }

    /**
     * 鉴于错误处理器的方法会始终返回 Error，并且 HTTP 状态码为 404（Not Found），那么现在我们可以对 spittleNotFound() 方法
     * 进行类似的清理。因为 spittleNotFound() 方法始终会返回 Error，所以使用 ResponseEntity 的唯一原因就是能够设置状态码。
     * 但是通过为 spittleNotFound() 方法添加 @ResponseStatus(HttpStatus.NOT_FOUND) 注解，我们可以达到相同的效果，而且可以
     * 不再使用 ResponseEntity 了。
     * 同样，如果控制器类上使用了 @RestController，那么就可以移除掉 @ResponseBody，让代码更加干净。
     * @param e
     * @return
     */
    @ExceptionHandler(SpittleNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public @ResponseBody Error spittleNotFound1(SpittleNotFoundException e) {
        long spittleId = e.getSpittleId();
        return new Error(4, "Spittle [" + spittleId + "] not found");
    }

    /**
     * 定义了错误处理器之后就能移除掉 spittleById() 方法中大多数的错误处理代码。这个版本的 spittleById() 方法确实干净了很多。除了对返回值进行 null 检查，它完全关注
     * 于成功的场景，也就是能够找到请求的 Spittle。同时，在返回类型中，我们能移除掉奇怪的泛型了。
     * @param id
     * @return
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public ResponseEntity<?> spittleById22(@PathVariable long id) {
        Spittle spittle = spittleRepository.findOne(id);
        if (spittle == null) {
            throw new SpittleNotFoundException(id);
        }
        return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
    }

    /**
     * 不过，我们能够让代码更加干净一些。现在我们已经知道 spittleById() 将会返回 Spittle 并且 HTTP 状态码始终会是 200（OK），那么就可以不再使用 ResponseEntity，
     * 而是将其替换为 @ResponseBody。当然，如果控制器类上使用了 @RestController，我们甚至不再需要 @ResponseBody。
     * @param id
     * @return
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public @ResponseBody Spittle spittleById3(@PathVariable long id) {
        Spittle spittle = spittleRepository.findOne(id);
        if (spittle == null) {
            throw new SpittleNotFoundException(id);
        }
        return spittle;
    }
}
